// Copyright © 2023 Cisco Systems, Inc. and its affiliates.
// All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package utils

import (
	"fmt"
	"strings"

	"github.com/openclarity/openclarity/scanner/families/malware/types"
)

const (
	// A yara detected malware line should be in the following format: <yara rule name> [<coma,separated,meta data>] <path>.
	ScanError          = "error scanning"
	SampleScanErrorNum = 10
	malwareNameKey     = "name" // The key defined in the metadata of yara rule
	malwareTypeKey     = "type" // The key defined in the metadata of yara rule
	ErrThreshold       = 0.8    // Error threshold
)

type ScanResultLine struct {
	RuleName string
	Metadata map[string]string
	Path     string
}

type InvalidLineError struct {
	Line string
}

func (e *InvalidLineError) Error() string {
	return "invalid yara line: " + e.Line
}

func IsErrorThresholdReached(errorCount, allCount uint) bool {
	if allCount != 0 && float64(errorCount)/float64(allCount) > ErrThreshold {
		return true
	}

	return false
}

func ParseYaraScanOutput(line string) (*types.Malware, error) {
	if strings.Contains(line, ScanError) {
		return nil, fmt.Errorf("yara scan failed: %s", line)
	}

	words := separateFields(line)
	if words == nil {
		return nil, &InvalidLineError{
			Line: line,
		}
	}

	return extractDetectedMalware(words), nil
}

func separateFields(line string) *ScanResultLine {
	var scanResultLine ScanResultLine
	ruleName, remainder, found := strings.Cut(line, "[")
	if !found {
		return nil
	}
	metadata, path, found := strings.Cut(remainder, "]")
	if !found {
		return nil
	}
	scanResultLine.RuleName = strings.TrimSpace(ruleName)
	scanResultLine.Metadata = parseYaraRuleMetadata(metadata)
	scanResultLine.Path = strings.TrimSpace(path)

	return &scanResultLine
}

func extractDetectedMalware(line *ScanResultLine) *types.Malware {
	if line == nil {
		return nil
	}

	// Extract name and type from metadata if they are defined
	var malwareName, malwareTypeStr string
	if _, ok := line.Metadata[malwareNameKey]; ok {
		malwareName = line.Metadata[malwareNameKey]
	}
	if _, ok := line.Metadata[malwareTypeKey]; ok {
		malwareTypeStr = line.Metadata[malwareTypeKey]
	}

	return &types.Malware{
		MalwareName: malwareName,
		MalwareType: malwareTypeStr,
		Path:        line.Path,
		RuleName:    line.RuleName,
	}
}

func parseYaraRuleMetadata(metadata string) map[string]string {
	m := make(map[string]string)
	if metadata == "" {
		return m
	}
	entries := strings.Split(metadata, ",")
	for _, e := range entries {
		key, value, found := strings.Cut(e, "=")
		if !found {
			continue
		}
		m[key] = strings.Trim(value, "\"")
	}

	return m
}
